/*
 * @(#)PolicyReference.java
 *
 * Copyright 2003-2005 Sun Microsystems, Inc. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   1. Redistribution of source code must retain the above copyright notice,
 *      this list of conditions and the following disclaimer.
 * 
 *   2. Redistribution in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 *
 * Neither the name of Sun Microsystems, Inc. or the names of contributors may
 * be used to endorse or promote products derived from this software without
 * specific prior written permission.
 * 
 * This software is provided "AS IS," without a warranty of any kind. ALL
 * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING
 * ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
 * OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MICROSYSTEMS, INC. ("SUN")
 * AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE
 * AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS
 * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST
 * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL,
 * INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY
 * OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE,
 * EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
 *
 * You acknowledge that this software is not designed or intended for use in
 * the design, construction, operation or maintenance of any nuclear facility.
 */

package org.wso2.balana;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.balana.combine.CombiningAlgorithm;

import org.wso2.balana.ctx.AbstractResult;
import org.wso2.balana.ctx.EvaluationCtx;
import org.wso2.balana.ctx.ResultFactory;
import org.wso2.balana.ctx.xacml2.Result;
import org.wso2.balana.ctx.Status;

import org.wso2.balana.finder.PolicyFinder;
import org.wso2.balana.finder.PolicyFinderResult;

import java.io.OutputStream;
import java.io.PrintStream;

import java.net.URI;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;

/**
 * This class is used as a placeholder for the PolicyIdReference and
 * PolicySetIdReference fields in a PolicySetType. When a reference is used in a
 * policy set, it is telling the PDP to use an external policy in the current
 * policy. Each time the PDP needs to evaluate that policy reference, it asks
 * the policy finder for the policy. Typically the policy finder will have
 * cached the referenced policy, so this isn't too slow.
 * <p>
 * NOTE: all of the accessor methods, the match method, and the evaluate method
 * require this class to ask its <code>PolicyFinder</code> for the referenced
 * policy, which can be a slow operation. Care should be taken, therefore in
 * calling these methods too often. Also note that it's not safe to cache the
 * results of these calls, since the referenced policy may change.
 * 
 * @since 1.0
 * @author Seth Proctor
 */
public class PolicyReference extends AbstractPolicy {

	/**
	 * Identifies this as a reference to a <code>Policy</code>
	 */
	public static final int POLICY_REFERENCE = 0;

	/**
	 * Identifies this as a reference to a <code>PolicySet</code>
	 */
	public static final int POLICYSET_REFERENCE = 1;

	// the reference
	private URI reference;

	// the reference type
	private int policyType;

	// and version constraints on this reference
	private VersionConstraints constraints;

	// the finder to use in finding the referenced policy
	private PolicyFinder finder;

	// the meta-data for the parent policy
	private PolicyMetaData parentMetaData;

	// the logger we'll use for all messages
	private static Log logger = LogFactory.getLog(PolicyReference.class);

	/**
	 * Creates a new <code>PolicyReference</code> instance. This has no
	 * constraints on version matching. Note that an XACML 1.x reference may not
	 * have any constraints.
	 * 
	 * @param reference
	 *            the reference to the policy
	 * @param policyType
	 *            one of the two fields in this class
	 * @param finder
	 *            the <code>PolicyFinder</code> used to handle the reference
	 * @param parentMetaData
	 *            the meta-data associated with the containing (parent) policy
	 * 
	 * @throws IllegalArgumentException
	 *             if the input policyType isn't valid
	 */
	public PolicyReference(URI reference, int policyType, PolicyFinder finder,
			PolicyMetaData parentMetaData) throws IllegalArgumentException {
		this(reference, policyType, new VersionConstraints(null, null, null),
				finder, parentMetaData);
	}

	/**
	 * Creates a new <code>PolicyReference</code> instance with version
	 * constraints. Note that an XACML 1.x reference may not have any
	 * constraints.
	 * 
	 * @param reference
	 *            the reference to the policy
	 * @param policyType
	 *            one of the two fields in this class
	 * @param constraints
	 *            any optional constraints on the version of the referenced
	 *            policy (this is never null, but it may impose no constraints,
	 *            and in fact will never impose constraints when used from a
	 *            pre-2.0 XACML policy)
	 * @param finder
	 *            the <code>PolicyFinder</code> used to handle the reference
	 * @param parentMetaData
	 *            the meta-data associated with the containing (parent) policy
	 * 
	 * @throws IllegalArgumentException
	 *             if the input policyType isn't valid
	 */
	public PolicyReference(URI reference, int policyType,
			VersionConstraints constraints, PolicyFinder finder,
			PolicyMetaData parentMetaData) throws IllegalArgumentException {

		// check if input policyType is a valid value
		if ((policyType != POLICY_REFERENCE)
				&& (policyType != POLICYSET_REFERENCE))
			throw new IllegalArgumentException("Input policyType is not a"
					+ "valid value");

		this.reference = reference;
		this.policyType = policyType;
		this.constraints = constraints;
		this.finder = finder;
		this.parentMetaData = parentMetaData;
	}

	/**
	 * Creates an instance of a <code>PolicyReference</code> object based on a
	 * DOM node.
	 * 
	 * @deprecated As of 2.0 you should avoid using this method and should
	 *             instead use the version that takes a
	 *             <code>PolicyMetaData</code> instance. This method will only
	 *             work for XACML 1.x policies.
	 * 
	 * @param root
	 *            the DOM root of a PolicyIdReference or a PolicySetIdReference
	 *            XML type
	 * @param finder
	 *            the <code>PolicyFinder</code> used to handle the reference
	 * @return an instance of PolicyReference
	 * @exception ParsingException
	 *                if the node is invalid
	 */
	public static PolicyReference getInstance(Node root, PolicyFinder finder)
			throws ParsingException {
		return getInstance(root, finder, new PolicyMetaData());
	}

	/**
	 * Creates an instance of a <code>PolicyReference</code> object based on a
	 * DOM node.
	 * 
	 * @param root
	 *            the DOM root of a PolicyIdReference or a PolicySetIdReference
	 *            XML type
	 * @param finder
	 *            the <code>PolicyFinder</code> used to handle the reference
	 * @param metaData
	 *            the meta-data associated with the containing policy
	 * @return an instance of PolicyReference
	 * @exception ParsingException
	 *                if the node is invalid
	 */
	public static PolicyReference getInstance(Node root, PolicyFinder finder,
			PolicyMetaData metaData) throws ParsingException {

		URI reference;
		int policyType;

		// see what type of reference we are
		String name = root.getNodeName();
		if (name.equals("PolicyIdReference")) {
			policyType = POLICY_REFERENCE;
		} else if (name.equals("PolicySetIdReference")) {
			policyType = POLICYSET_REFERENCE;
		} else {
			throw new ParsingException("Unknown reference type: " + name);
		}

		// next get the reference
		try {
			reference = new URI(root.getFirstChild().getNodeValue());
		} catch (Exception e) {
			throw new ParsingException("Invalid URI in Reference", e);
		}

		// now get any constraints
		NamedNodeMap map = root.getAttributes();

		String versionConstraint = null;
		Node versionNode = map.getNamedItem("Version");
		if (versionNode != null)
			versionConstraint = versionNode.getNodeValue();

		String earlyConstraint = null;
		Node earlyNode = map.getNamedItem("EarliestVersion");
		if (earlyNode != null)
			earlyConstraint = earlyNode.getNodeValue();

		String lateConstraint = null;
		Node lateNode = map.getNamedItem("LatestVersion");
		if (lateNode != null)
			lateConstraint = lateNode.getNodeValue();

		VersionConstraints constraints = new VersionConstraints(
				versionConstraint, earlyConstraint, lateConstraint);

		// finally, create the reference
		return new PolicyReference(reference, policyType, constraints, finder,
				metaData);
	}

	/**
	 * Returns the reference identifier used to resolve the policy.
	 * 
	 * @return the reference <code>URI</code>
	 */
	public URI getReference() {
		return reference;
	}

	/**
	 * Returns the version constraints associated with this reference. This will
	 * never be null, though the constraints may be empty.
	 * 
	 * @return the version constraints
	 */
	public VersionConstraints getConstraints() {
		return constraints;
	}

	/**
	 * Returns whether this is a reference to a policy or to a policy set.
	 * 
	 * @return the reference type, either <code>POLICY_REFERENCE</code> or
	 *         <code>POLICYSET_REFERENCE</code>
	 */
	public int getReferenceType() {
		return policyType;
	}

	/**
	 * Returns the id of this policy. If the policy is invalid or can't be
	 * retrieved, then a runtime exception is thrown.
	 * 
	 * @return the policy id
	 * 
	 * @throws ProcessingException
	 *             if the referenced policy can't be retrieved
	 */
	public URI getId() {
		return resolvePolicy().getId();
	}

	/**
	 * Returns the version of this policy. If the policy is invalid or can't be
	 * retrieved, then a runtime exception is thrown.
	 * 
	 * @return the policy version
	 * 
	 * @throws ProcessingException
	 *             if the referenced policy can't be retrieved
	 */
	public String getVersion() {
		return resolvePolicy().getVersion();
	}

	/**
	 * Returns the combining algorithm used by this policy. If the policy is
	 * invalid or can't be retrieved, then a runtime exception is thrown.
	 * 
	 * @return the combining algorithm
	 * 
	 * @throws ProcessingException
	 *             if the referenced policy can't be retrieved
	 */
	public CombiningAlgorithm getCombiningAlg() {
		return resolvePolicy().getCombiningAlg();
	}

	/**
	 * Returns the given description of this policy or null if there is no
	 * description. If the policy is invalid or can't be retrieved, then a
	 * runtime exception is thrown.
	 * 
	 * @return the description or null
	 * 
	 * @throws ProcessingException
	 *             if the referenced policy can't be retrieved
	 */
	public String getDescription() {
		return resolvePolicy().getDescription();
	}

	/**
	 * Returns the target for this policy. If the policy is invalid or can't be
	 * retrieved, then a runtime exception is thrown.
	 * 
	 * @return the policy's target
	 * 
	 * @throws ProcessingException
	 *             if the referenced policy can't be retrieved
	 */
	public AbstractTarget getTarget() {
		return resolvePolicy().getTarget();
	}

	/**
	 * Returns the default version for this policy. If the policy is invalid or
	 * can't be retrieved, then a runtime exception is thrown.
	 * 
	 * @return the policy's default version
	 * 
	 * @throws ProcessingException
	 *             if the referenced policy can't be retrieved
	 */
	public String getDefaultVersion() {
		return resolvePolicy().getDefaultVersion();
	}

	/**
	 * Returns the child policy nodes under this node in the policy tree. If the
	 * policy is invalid or can't be retrieved, then a runtime exception is
	 * thrown.
	 * 
	 * @return the <code>List</code> of child policy nodes
	 * 
	 * @throws ProcessingException
	 *             if the referenced policy can't be retrieved
	 */
	public List getChildren() {
		return resolvePolicy().getChildren();
	}

	/**
	 * Returns the child policy nodes and their associated parameters. If the
	 * policy is invalid or can't be retrieved, then a runtime exception is
	 * thrown.
	 * 
	 * @return a <code>List</code> of <code>CombinerElement</code>s
	 * 
	 * @throws ProcessingException
	 *             if the referenced policy can't be retrieved
	 */
	public List getChildElements() {
		return resolvePolicy().getChildElements();
	}

	/**
	 * Returns the Set of obligations for this policy, which may be empty if
	 * there are no obligations. If the policy is invalid or can't be retrieved,
	 * then a runtime exception is thrown.
	 * 
	 * @return the policy's obligations
	 * 
	 * @throws ProcessingException
	 *             if the referenced policy can't be retrieved
	 */
	public Set getObligationExpressions() {
		return resolvePolicy().getObligationExpressions();
	}

	/**
	 * Returns the meta-data associated with this policy. If the policy is
	 * invalid or can't be retrieved, then a runtime exception is thrown. Note
	 * that this is the meta-data for the referenced policy, not the meta-data
	 * for the parent policy (which is what gets provided to the constructors of
	 * this class).
	 * 
	 * @return the policy's meta-data
	 * 
	 * @throws ProcessingException
	 *             if the referenced policy can't be retrieved
	 */
	public PolicyMetaData getMetaData() {
		return resolvePolicy().getMetaData();
	}

	/**
	 * Given the input context sees whether or not the request matches this
	 * policy. This must be called by combining algorithms before they evaluate
	 * a policy. This is also used in the initial policy finding operation to
	 * determine which top-level policies might apply to the request. If the
	 * policy is invalid or can't be retrieved, then a runtime exception is
	 * thrown.
	 * 
	 * @param context
	 *            the representation of the request
	 * 
	 * @return the result of trying to match the policy and the request
	 */
	public MatchResult match(EvaluationCtx context) {
		try {
			return getTarget().match(context);
		} catch (ProcessingException pe) {
			// this means that we couldn't resolve the policy
			ArrayList code = new ArrayList();
			code.add(Status.STATUS_PROCESSING_ERROR);
			Status status = new Status(code, "couldn't resolve policy ref");
			return new MatchResult(MatchResult.INDETERMINATE, status);
		}
	}

	/**
	 * Private helper method that tried to resolve the policy
	 */
	private AbstractPolicy resolvePolicy() {
		// see if this reference was setup with a finder
		if (finder == null) {
			if (logger.isWarnEnabled()) {
				logger.warn("PolicyReference with id " + reference.toString()
						+ " was queried but was "
						+ "not configured with a PolicyFinder");
			}

			throw new ProcessingException("couldn't find the policy with "
					+ "a null finder");
		}

		PolicyFinderResult pfr = finder.findPolicy(reference, policyType,
				constraints, parentMetaData);

		if (pfr.notApplicable())
			throw new ProcessingException("couldn't resolve the policy");

		if (pfr.indeterminate())
			throw new ProcessingException("error resolving the policy");

		return pfr.getPolicy();
	}

	/**
	 * Tries to evaluate the policy by calling the combining algorithm on the
	 * given policies or rules. The <code>match</code> method must always be
	 * called first, and must always return MATCH, before this method is called.
	 * 
	 * @param context
	 *            the representation of the request
	 * 
	 * @return the result of evaluation
	 */
	public AbstractResult evaluate(EvaluationCtx context) {
		// if there is no finder, then we return NotApplicable
		if (finder == null) {
			// return new Result(Result.DECISION_NOT_APPLICABLE,
			// context.getResourceId().encode());
			return ResultFactory.getFactory().getResult(
					Result.DECISION_NOT_APPLICABLE, context);
		}

		PolicyFinderResult pfr = finder.findPolicy(reference, policyType,
				constraints, parentMetaData);

		MatchResult match = pfr.getPolicy().match(context);

		// if we found nothing, then we return NotApplicable
		if (pfr.notApplicable() || match.getResult() == MatchResult.NO_MATCH) {
			// return new Result(Result.DECISION_NOT_APPLICABLE,
			// context.getResourceId().encode());
			return ResultFactory.getFactory().getResult(
					Result.DECISION_NOT_APPLICABLE, context);
		}
		// if there was an error, we return that status data
		if (pfr.indeterminate()) {
			// return new Result(Result.DECISION_INDETERMINATE, pfr.getStatus(),
			// context
			// .getResourceId().encode());
			return ResultFactory.getFactory().getResult(
					Result.DECISION_INDETERMINATE, pfr.getStatus(), context);
		}
		// we must have found a policy
		return pfr.getPolicy().evaluate(context);
	}

	/**
	 * Encodes this <code>PolicyReference</code> into its XML representation and
	 * writes this encoding to the given <code>OutputStream</code> with no
	 * indentation.
	 * 
	 * @param output
	 *            a stream into which the XML-encoded data is written
	 */
	public void encode(OutputStream output) {
		encode(output, new Indenter(0));
	}

	/**
	 * Encodes this <code>PolicyReference</code> into its XML representation and
	 * writes this encoding to the given <code>OutputStream</code> with
	 * indentation.
	 * 
	 * @param output
	 *            a stream into which the XML-encoded data is written
	 * @param indenter
	 *            an object that creates indentation strings
	 */
	public void encode(OutputStream output, Indenter indenter) {
		PrintStream out = new PrintStream(output);
		String encoded = indenter.makeString();

		if (policyType == POLICY_REFERENCE) {
			out.println(encoded + "<PolicyIdReference" + encodeConstraints()
					+ ">" + reference.toString() + "</PolicyIdReference>");
		} else {
			out.println(encoded + "<PolicySetIdReference" + encodeConstraints()
					+ ">" + reference.toString() + "</PolicySetIdReference>");
		}
	}

	/**
	 * Private helper method that encodes the variable constraints info. Note
	 * that if this is a pre-2.0 policy the constraints are always null, so
	 * nothing will be added here.
	 */
	private String encodeConstraints() {
		String str = "";
		VersionConstraints version = getConstraints();

		String v = version.getVersionConstraint();
		if (v != null)
			str += " Version=\"" + v + "\"";

		String e = version.getEarliestConstraint();
		if (e != null)
			str += " EarliestVersion=\"" + e + "\"";

		String l = version.getLatestConstraint();
		if (l != null)
			str += " LatestVersion=\"" + l + "\"";

		return str;
	}

}
